Привет, Хабр! Меня зовут Оля, и я старший инженер по тестированию в Lineate. Хочу рассказать о своей попытке осознать SOLID принципы и понять, где их место в автоматизированном тестировании.
Сегодня можно найти тысячи статей о SOLID. Только на Хабре их как минимум пара десятков. Эту я пишу по двум причинам: за время изучения не видела материала, в котором бы все принципы SOLID раскрывались на сквозном примере, и в сети нашла минимум информации про применение SOLID в автоматизации тестирования.
Соответственно, этот материал состоит из двух частей:
-
в первой возьмем простое приложение на Java и улучшим его с помощью SOLID принципов — от программы с парой классов, которые делают все подряд, дойдем до приложения, разбитого на несколько модулей с конкретными функциями (да, это еще одно объяснение SOLID, смело пропускайте, если уже и так все знаете);
-
во второй части посмотрим, где во фреймворках автоматизированного тестирования может использоваться SOLID.
SOLID: базовые факты
SOLID — это пять основных принципов объектно-ориентированного программирования и проектирования кода.
Вот что стоит за каждой из букв в обозначении:
S — SRP, Single Responsibility Principle (Принцип единственной ответственности)
O — OCP, Open/ Closed Principle (Принцип открытости/ закрытости)
L — LSP, Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
I — ISP, Interface Segregation Principle (Принцип разделения интерфейсов)
D — DIP, Dependency Inversion Principle (Принцип инверсии зависимостей)
Принципы SOLID не были искусственно придуманы теоретиками, а скорее обобщают коллективные знания и опыт разработчиков. После нескольких десятилетий работы Роберт «Дядюшка Боб» Мартин объединил их в единую концепцию. А звучный акроним SOLID предложил Майкл Физерс.
Основными бонусами от использования SOLID принципов должны стать:
-
простой и понятный код, который потребует минимального времени на вхождение от нового разработчика;
-
стабильный код, в который можно максимально безболезненно встраивать новые фичи, запрошенные заказчиком;
-
код с низкой связанностью, над которым в параллель могут работать несколько разработчиков;
-
минимальное количество регрессионных багов при внесении изменений в существующий код.
SOLID принципы используются на уровне модулей и классов в программах, построенных в парадигме ООП.
Существуют и другие подходы к проектированию кода. Это методологии GRASP, DRY, KISS, YAGNI и др. Каждый из подходов имеет свои особенности, но суть одна — сделать код более простым и удобным для активной разработки и поддержки.
Тестовое приложение
Чтобы разобраться, как работают SOLID принципы, я написала маленькое и очень простое приложение на Java. Представим, что этим приложением будут пользоваться оформители интерьера, дизайнеры и строители. Задача программы – вычислять площадь геометрических фигур в чистом виде, а также площадь с небольшим коэффициентом. Первый вариант использования – посчитать чистую площадь пола в помещении для внесения в смету или на эскиз. Второй – рассчитать, сколько нужно купить плитки с небольшим запасом, чтобы покрыть пол, или сколько купить краски, чтобы покрасить стену.
Будем считать, что это развивающийся продукт и на данном этапе он имеет некоторые функции и ограничения. Например, тип фигуры определяется не программно, а пользователем через консоль, как и измерения фигуры, необходимые для вычисления площади. Результат вычислений также выводится в консоль.
Так выглядит структура проекта в самом начале (ветка SRP-1):
В проекте есть пакет models
, где лежит enum Figure
. Тут перечислены все типы фигур, которые поддерживаются приложением на данный момент.
Здесь же, в пакете models
, лежат объекты самих фигур. К примеру, треугольник выглядит так:
@Data public class Triangle { private Double baseLength; private Double height; }
Я использую библиотеку Lombok, поэтому не прописываю явно конструктор, сеттеры и геттеры, они здесь есть, но за счет аннотации скрыты.
Также в приложении есть несколько классов, в которых описана основная логика. Это все, что касается взаимодействия с пользователем и собственно вычисления площади.
В UserInteraction
– методы для общения с пользователем:
public class UserInteraction { //... public Figure readFigureFromInput() { //ask user to enter figure in console and return figure } public String readAreaTypeFromInput(Figure figure) { //ask user to enter area type in console and return area type } public void printAreaInConsole(Figure figure, String areaType, Double area) { //print area in console } }
Класс CalculateArea – точка входа для вычисления площади. Именно его метод calculateArea(Figure figure, String areaType)
вызывается в исполняемом классе Main
. В этом методе в зависимости от типа фигуры и типа площади вызываются методы для вычислений. Если нужно посчитать чистую площадь без коэффициентов, используем методы из этого же класса, а если нужна площадь под покраску или для плитки, используем инстанс класса CalculateDecorationArea
и его методы для фигур.
public class CalculateArea { private CalculateDecorationArea calculateDecorationArea = new CalculateDecorationArea(); public Double calculateArea(Figure figure, String areaType) { Double area = null; if (areaType == "simple") { if (figure == Figure.CIRCLE) { area = calculateCircleArea(); } else if (figure == Figure.SQUARE) { area = calculateSquareArea(); } else if (figure == Figure.TRIANGLE) { area = calculateTriangleArea(); } } else if (areaType == "painting") { area = calculateDecorationArea.calculateDecorationArea(figure); } else if (areaType == "tile") { area = calculateDecorationArea.calculateDecorationArea(figure); } return area; } public Double calculateSquareArea() { //user input and calculations for square } public double calculateCircleArea() { //user input and calculations for circle } public double calculateTriangleArea() { //user input and calculations for triangle } }
CalculateDecorationArea
выглядит очень похоже и отличается только применением коэффициента в формуле расчета площади.
Если попробовать запустить приложение, можно увидеть, что оно вполне нормально работает (дисклеймер: работают только основные кейсы, в коде нет никакой обработки ошибок).
Что здесь не так? Ведь код компилируется, и программа выполняет свои функции. Посмотрим на приложение с точки зрения SOLID принципов.
Окунемся в SOLID на живом примере
Пришло время более подробно познакомиться с каждым из принципов SOLID и посмотреть, как они работают на улучшение кода.
S – SRP, Single Responsibility Principle
Первый из принципов. Роберт Мартин в своей книге «Чистая архитектура» расшифровывает его так:
Модуль должен иметь одну и только одну причину для изменения
ПО меняется в ответ на запросы пользователей → Модуль должен отвечать за одну и только одну группу пользователей или заинтересованных лиц, то есть за одного актора → Модуль должен отвечать за одного и только одного актора.
Опасность представляют модули и классы, которые обслуживают сразу нескольких потребителей или акторов (это могут быть живые пользователи приложения или другие модули программы) и меняются в зависимости от их требований.
Спустимся на уровень классов. Если класс делает вычисления, что-то куда-то отправляет, выводит, описывает логику логирования – это нарушение SRP и антипаттерн. Такой объект можно назвать «божественный объект» или God object.
Чтобы лучше понять этот принцип, обратимся к тестовому приложению.
Пример из приложения
Первый и явный пример – конфликт интересов маляров и плиточников. Класс CalculateDecorationArea
содержит код, которым пользуются эти две группы акторов. К каким проблемам это может привести? Например, маляры поймут, что в программе заложен слишком большой коэффициент. Допустим, они захотят его поменять, чтобы при закупке краски не оставалось излишков. Но что если для кафельной плитки такие коэффициенты вполне подходят? Получается, с одной стороны у нас есть маляры, а с другой — укладчики плитки, и обе эти группы пользуются одним и тем же кодом для расчета площади. Если код поменяется с учетом новых требований от маляров, плитки будет закуплено слишком мало. Соответственно, в приложении появится дефект с точки зрения плиточников.
Лучше в этом случае разделить вычисления и создать два разных класса. В ветке SRP-1 как раз появляются два отдельных класса CalculatePaintingArea
— для маляров с их коэффициентом и CalculateTileArea
– для плиточников.
Пример для маляров с обновленным коэффициентом:
public class CalculatePaintingArea { private static final Double PAINTING_COEFFICIENT = 1.1; public Double calculatePaintingArea(Figure figure) { Double paintingArea = null; if (figure == Figure.CIRCLE) { paintingArea = calculateCirclePaintingArea(); } else if (figure == Figure.SQUARE) { paintingArea = calculateSquarePaintingArea(); } else if (figure == Figure.TRIANGLE) { paintingArea = calculateTrianglePaintingArea(); } return paintingArea; } public double calculateSquarePaintingArea() { //user input and calculations for square with coefficient } public double calculateCirclePaintingArea() { //user input and calculations for square with coefficient } public double calculateTrianglePaintingArea() { Double triangleArea; Scanner sn = new Scanner(System.in); System.out.println("Enter the length of the triangle base: "); Double length = sn.nextDouble(); System.out.println("Enter the length of the triangle height: "); Double height = sn.nextDouble(); triangleArea = length * height / 2 * PAINTING_COEFFICIENT; return triangleArea; } }
Можно сказать,